﻿using System;
using System.Diagnostics.Contracts;
using System.Reactive;
using System.Reactive.Concurrency;
using System.Reactive.Disposables;
using System.Reactive.Linq;

namespace Rxx.Parsers.Reactive
{
	/// <summary>
	/// Represents an ambiguous parser that begins a parse operation at the current position of the source sequence.
	/// </summary>
	/// <typeparam name="TSource">The type of the source elements.</typeparam>
	/// <typeparam name="TUntilResult">The type of the elements that are generated by the optional until parser.</typeparam>
	/// <typeparam name="TResult">The type of the elements that are generated from parsing the source elements.</typeparam>
	internal sealed class AmbiguousObservableParser<TSource, TUntilResult, TResult> : IObservableParser<TSource, IObservable<TResult>>
	{
		#region Public Properties
		public IObservableParser<TSource, TSource> Next
		{
			get
			{
				return parser.Next;
			}
		}
		#endregion

		#region Private / Protected
		private const int unlimitedCount = -1;

		private readonly IObservableParser<TSource, TResult> parser;
		private readonly IObservableParser<TSource, TUntilResult> untilParser;
		private readonly int untilCount;
		#endregion

		#region Constructors
		public AmbiguousObservableParser(IObservableParser<TSource, TResult> parser)
		{
			Contract.Requires(parser != null);

			this.parser = parser;
			this.untilCount = unlimitedCount;
		}

		public AmbiguousObservableParser(IObservableParser<TSource, TResult> parser, int untilCount)
		{
			Contract.Requires(parser != null);
			Contract.Requires(untilCount >= 0);

			this.parser = parser;
			this.untilCount = untilCount;
		}

		public AmbiguousObservableParser(IObservableParser<TSource, TResult> parser, IObservableParser<TSource, TUntilResult> untilParser)
		{
			Contract.Requires(parser != null);
			Contract.Requires(untilParser != null);

			this.parser = parser;
			this.untilCount = unlimitedCount;
			this.untilParser = untilParser;
		}
		#endregion

		#region Methods
		[ContractInvariantMethod]
		private void ObjectInvariant()
		{
			Contract.Invariant(parser != null);
			Contract.Invariant(untilCount >= unlimitedCount);
		}

		public IObservable<IParseResult<IObservable<TResult>>> Parse(IObservableCursor<TSource> source)
		{
			return Observable.Create<IParseResult<IObservable<TResult>>>(
				observer =>
				{
					int matchCount = 0;
					int remainingLength = 0;

					Action<Action> iterate = moveNext =>
					{
						bool hasResult = false;
						int length = 0;

						var branch = source.Branch();

						var values = parser.Parse(branch)
							.Finally(branch.Dispose)
							.Select(result =>
								{
									if (!hasResult)
									{
										matchCount++;
										hasResult = true;
									}

									length = Math.Max(length, result.Length);

									return result.Value;
								})
							.Do(
								__ => { },
								() =>
								{
									/* We must respect the greediness of the results unless the length is zero since the
									 * cursor would have already moved to the following element.  It is acceptable to ignore
									 * zero-length results because marking an entirely non-greedy parser as ambiguous would
									 * otherwise cause the parser to continously parse the first element indefinitely.
									 */
									if (length > 0)
									{
										remainingLength = length - 1;
									}
									else if (remainingLength > 0)
									{
										remainingLength--;
									}

									moveNext();
								});

						observer.OnNext(ParseResult.Create(values, length: 1));
					};

					Action complete = () =>
						{
							if (remainingLength > 0)
							{
								observer.OnNext(ObservableParseResult.SuccessMany<TResult>(remainingLength));
							}

							observer.OnCompleted();
						};

					var untilSubscription = new SerialDisposable();

					var schedule = Scheduler.Immediate.Schedule(
						self =>
						{
							if (!source.AtEndOfSequence
								&& (untilCount == unlimitedCount || matchCount < untilCount))
							{
								if (untilParser == null)
								{
									iterate(self);
								}
								else
								{
									untilSubscription.SetDisposableIndirectly(() =>
										untilParser.Parse(source).Any().Subscribe(
											any =>
											{
												if (!any)
												{
													iterate(self);
												}
												else
												{
													complete();
												}
											},
											observer.OnError));
								}
							}
							else
							{
								complete();
							}
						});

					return new CompositeDisposable(schedule, untilSubscription);
				});
		}
		#endregion
	}
}